package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.api.R;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

/**
 * redis缓存工具类
 */
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将java对象序列化为JSON存储
     *
     * @param key
     * @param value
     * @param time     TTL
     * @param timeUnit 单位
     */
    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /**
     * 连带逻辑过期
     *
     * @param key
     * @param value
     * @param time
     * @param timeUnit
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        //设置逻辑过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 互斥锁实现解决缓存穿透
     *
     * @param preKey     key的前缀
     * @param id         不定类型的id
     * @param type       对象类型
     * @param dbFallBack 函数式编程，传进的数据库降级函数
     * @param time       时间
     * @param timeUnit   时间单位
     * @param <T>        返回对象类型
     * @param <ID>       id类型
     * @return
     * @return
     */
    public <T, ID> T queryWithPassThrough(
            String preKey, ID id, Class<T> type, Function<ID, T> dbFallBack, Long time, TimeUnit timeUnit) {
        //1.从Redis内查询缓存
        String key = preKey + id;
        //1.从Redis内查询商品缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            //手动反序列化
            return JSONUtil.toBean(json, type);
        }
        //如果上面的判断不对，那么就是我们设置的""(有缓存"",证明数据库内肯定是没有的)或者null(没有缓存)
        //判断命中的是否是空值
        if (json != null) {
            return null;
        }
        //2.不存在就根据id查询数据库
        T t = dbFallBack.apply(id);//由于不知道这段逻辑，所以我们需要用户传进来函数逻辑
        if (t == null) {
            //将空值写进入
            stringRedisTemplate.opsForValue().set(key, "", 2L, TimeUnit.MINUTES);
            return null;
        }
        //写入Redis
        this.set(key, t, time, timeUnit);
        return t;
    }


    /**
     * 获取互斥锁
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);  //需要将flag转成基本类型，并且为了避免自动 拆箱（拆箱有可能出现null值）使用工具类
        //isTrue:只有当flag是true才是true，flag为false和null都返回false
    }

    /**
     * 释放互斥锁
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    //开启10个线程
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑过期实现解决缓存击穿
     **/
    public <T, ID> T queryWithLogicalExpire(
            String preKey, ID id, Class<T> type, Function<ID, T> dbFallBack, Long time, TimeUnit timeUnit) {
        //1.从Redis内查询商品缓存
        String key = preKey + id;
        String redisDataJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isBlank(redisDataJson)) {
            //存在直接返回
            return null;
        }
        //3.命中，需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        T t1 = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //3，命中则判断是否过期，未过期则返回老数据shop
        if (expireTime.isAfter(LocalDateTime.now())) {
            return t1;
        }
        //4，过期则需要缓存重建
        //5，尝试获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        //6，判断是否获取锁
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            //6.3,成功。开启独立线程，实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(
                    () -> {
                        //重建缓存
                        try {
                            //查询数据库
                            T t2 = dbFallBack.apply(id);
                            //连带逻辑过期写入Redis
                            setWithLogicalExpire(key, t2, time, timeUnit);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        } finally {
                            //释放锁
                            unLock(lockKey);
                        }
                    });
        }
        return t1;
    }
}